package lovely.gwt.jsonrpc.rebind;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import rocket.generator.rebind.GeneratorContext;
import rocket.generator.rebind.GeneratorHelper;
import rocket.generator.rebind.method.Method;
import rocket.generator.rebind.method.NewMethod;
import rocket.generator.rebind.methodparameter.MethodParameter;
import rocket.generator.rebind.methodparameter.NewMethodParameter;
import rocket.generator.rebind.type.NewConcreteType;
import rocket.generator.rebind.type.Type;
import rocket.generator.rebind.visitor.AllMethodsVisitor;
import rocket.remoting.rebind.rpc.json.JsonRpcClientGenerator;
import rocket.remoting.rebind.rpc.json.post.JsonConstants;
import rocket.remoting.rebind.rpc.json.requestparameters.RequestParametersConstants;
import rocket.util.client.Checker;

import lovely.gwt.jsonrpc.rebind.post.JsonRpcInvokerTemplatedFile;
import lovely.gwt.jsonrpc.rebind.post.RequestParametersInvokerTemplatedFile;

/**
 * This generator generates a client proxy that may be used to invoke a
 * json encoded rpc on the server (json-rpc specification 1.1).
 * 
 * @author Stefan Martin
 */

public class JsonRpc11ClientGenerator extends JsonRpcClientGenerator {

	/**
	 * Adds a method to the jsonServiceClient that prepares an invoker etc.
	 * 
	 * @param method
	 *            The method being implemented
	 * @param serviceInterface
	 *            The service interface being implemented
	 * @param asyncServiceInterface
	 *            The matching async service interface
	 * @param remoteJsonClient
	 *            The remote
	 */
	protected void implementPublicMethod(final Method method, final Type serviceInterface, final Type asyncServiceInterface,
			final NewConcreteType remoteJsonClient) {
		Checker.notNull("parameter:method", method);
		Checker.notNull("parameter:serviceInterface", serviceInterface);
		Checker.notNull("parameter:asyncServiceInterface", asyncServiceInterface);
		Checker.notNull("parameter:remoteJsonClient", remoteJsonClient);

		while (true) {
			final String inputArguments = this.getInputArgumentEncodingFromMethodAnnotation(method);
			if (Constants.INPUT_ARGUMENTS_JSON_RPC.equals(inputArguments)) {
				this.implementJsonRpcMethod(method, asyncServiceInterface, remoteJsonClient);
				break;
			}
			if (Constants.INPUT_ARGUMENTS_REQUEST_PARAMETERS.equals(inputArguments)) {
				this.implementRequestParameters(method, asyncServiceInterface, remoteJsonClient);
				break;
			}

			throw new RuntimeException();
		}

	}

	/**
	 * Implements a method that is a bridge that invokes a json rpc encoded
	 * response and deserializes responses calling the provided callback.
	 * 
	 * @param method
	 */
	protected void implementJsonRpcMethod(final Method method, final Type asyncServiceInterface, final NewConcreteType remoteJsonClient) {
		Checker.notNull("parameter:method", method);
		Checker.notNull("parameter:asyncServiceInterface", asyncServiceInterface);
		Checker.notNull("parameter:remoteJsonClient", remoteJsonClient);

		this.getGeneratorContext().info("Implementing json rpc method, method: " + method);

		final NewMethod asyncMethod = this.createCorrespondingAsyncServiceInterfaceMethod(method, asyncServiceInterface,
				remoteJsonClient);
		final List parameters = asyncMethod.getParameters();
		if (parameters.size() != 2) {
			throwInvalidJsonRpcMethod(method);
		}

		final NewMethodParameter parameter = (NewMethodParameter) parameters.get(0);
		parameter.setName(JsonConstants.INVOKER_PARAMETER_PARAMETER);
		final NewMethodParameter callback = (NewMethodParameter) parameters.get(1);
		callback.setName(JsonConstants.INVOKER_CALLBACK_PARAMETER);

		final JsonRpcInvokerTemplatedFile body = new JsonRpcInvokerTemplatedFile();
		Type parameterType = parameter.getType();
		Type returnType = getReturnComponentType(method);
		Type serializerType = null;
		if (returnType != null) {
			serializerType = getSerializerType(returnType);
		}
		String returnTemplateKey = getTemplateKey(method.getReturnType());
		String parameterTemplateKey = getTemplateKey(parameterType);
		body.setParameterType(parameterType);
		body.setRequestParameterType("object");
		body.setReturnType(returnType);
		body.setSerializerType(serializerType);
		body.setReturnTemplateKey(returnTemplateKey);
		body.setParameterTemplateKey(parameterTemplateKey);
		body.setMethod(asyncMethod);
		asyncMethod.setBody(body);
	}

	/**
	 * This method satisfy the currently being generated method passing simple
	 * parameters via request parameters
	 * 
	 * @param method
	 */
	protected void implementRequestParameters(final Method method, final Type asyncServiceInterface,
			final NewConcreteType remoteJsonClient) {
		Checker.notNull("parameter:method", method);
		Checker.notNull("parameter:asyncServiceInterface", asyncServiceInterface);
		Checker.notNull("parameter:remoteJsonClient", remoteJsonClient);

		this.getGeneratorContext().debug("Implementing method that sends method parameters as request parameters, method: " + method);
		final NewMethod asyncMethod = this.createCorrespondingAsyncServiceInterfaceMethod(method, asyncServiceInterface,
				remoteJsonClient);

		final RequestParametersInvokerTemplatedFile body = new RequestParametersInvokerTemplatedFile();
		body.setHttpRequestParameterNames(this.getHttpRequestParameterNamesFromMethodAnnotation(method));
		body.setParameters(asyncMethod.getParameters());
		body.setRequestParameterType(this.getRequestParametersTypeFromMethodAnnotation(method));
		Type returnType = getReturnComponentType(method);
		Type serializerType = null;
		if (returnType != null) {
			serializerType = getSerializerType(returnType);
		}
		String templateKey = getTemplateKey(method.getReturnType());

		body.setReturnType(returnType);
		body.setSerializerType(serializerType);
		body.setTemplate(templateKey);
		body.setMethod(asyncMethod);

		final Iterator newMethodParameters = asyncMethod.getParameters().iterator();
		while (newMethodParameters.hasNext()) {
			final NewMethodParameter parameter = (NewMethodParameter) newMethodParameters.next();
			parameter.setFinal(true);

			if (false == newMethodParameters.hasNext()) {
				parameter.setName(RequestParametersConstants.REQUEST_PARAMETERS_CALLBACK_PARAMETER);
			}
		}

		asyncMethod.setBody(body);
	}

	protected String getRequestParametersTypeFromMethodAnnotation(final Method method) {
		Checker.notNull("parameter:method", method);

		final List values = method.getMetadataValues(Constants.HTTP_REQUEST_PARAMETER_NAMED_ANNOTATION);
		String parametersType = "object";
		if (values.size() != 0) {
			parametersType = (String) values.get(0);
		}
		parametersType.toLowerCase();
		if (!parametersType.equals("object") && !parametersType.equals("array")) {
			this.throwRequestParametersTypeAnnotationException(method);
		}
		return parametersType;
	}

	protected void throwRequestParametersTypeAnnotationException(final Method method) {
		this.throwException("The method " + method + " has an invalid value " + Constants.HTTP_REQUEST_PARAMETER_NAMED_ANNOTATION
				+ ". array or object");
	}

	protected String getTemplateKey(final Type type) {
		if (type.toString().equals("void")) {
			return "void";
		}
		String name;
		if (type.isArray()) {
			name = "array";
		} else {
			name = "object";
		}
		return name;
	}

	protected Type getReturnComponentType(final Method method) {
		Type returnComponentType = method.getReturnType();
		if (returnComponentType.isArray()) {
			return returnComponentType.getComponentType();
		}
		return returnComponentType;
	}

	protected Type getSerializerType(final Type returnType) {
		Type serializer = null;
		serializer = getSerializerType0(returnType);
		if (serializer != null) {
			return serializer;
		} else {
			return returnType;
		}
	}

	protected Type getSerializerType0(final Type type) {
		Type serializer = null;
		while (true) {
			final GeneratorContext context = this.getGeneratorContext();
			{
				final Type booleanType = context.getBoolean();
				if (type.equals(booleanType) || type.equals(booleanType.getWrapper())) {
					serializer = context.getType(Constants.BOOLEAN_SERIALIZER);
					break;
				}
			}
			{
				final Type byteType = context.getByte();
				if (type.equals(byteType) || type.equals(byteType.getWrapper())) {
					serializer = context.getType(Constants.BYTE_SERIALIZER);
					break;
				}
			}

			{
				final Type shortType = context.getShort();
				if (type.equals(shortType) || type.equals(shortType.getWrapper())) {
					serializer = context.getType(Constants.SHORT_SERIALIZER);
					break;
				}
			}

			{
				final Type intType = context.getInt();
				if (type.equals(intType) || type.equals(intType.getWrapper())) {
					serializer = context.getType(Constants.INT_SERIALIZER);
					break;
				}
			}

			{
				final Type longType = context.getLong();
				if (type.equals(longType) || type.equals(longType.getWrapper())) {
					serializer = context.getType(Constants.LONG_SERIALIZER);
					break;
				}
			}

			{
				final Type floatType = context.getFloat();
				if (type.equals(floatType) || type.equals(floatType.getWrapper())) {
					serializer = context.getType(Constants.FLOAT_SERIALIZER);
					break;
				}
			}

			{
				final Type doubleType = context.getDouble();
				if (type.equals(doubleType) || type.equals(doubleType.getWrapper())) {
					serializer = context.getType(Constants.DOUBLE_SERIALIZER);
					break;
				}
			}

			{
				final Type charType = context.getChar();
				if (type.equals(charType) || type.equals(charType.getWrapper())) {
					serializer = context.getType(Constants.CHAR_SERIALIZER);
					break;
				}
			}
			if (type.equals(context.getString())) {
				serializer = context.getType(Constants.STRING_SERIALIZER);
				break;
			}
			break;
		}
		return serializer;
	}

	/**
	 * Attempts to find the corresponding method on the async interface.
	 * 
	 * @param method
	 *            A service interface method
	 * @param asyncServiceInterface
	 *            The async service interface
	 * @param client
	 *            The client being generated.
	 * @return The matching async service interface method.
	 */
	protected NewMethod createCorrespondingAsyncServiceInterfaceMethod(final Method method, final Type asyncServiceInterface,
			final NewConcreteType client) {
		Checker.notNull("parameter:method", method);
		Checker.notNull("parameter:asyncServiceInterface", asyncServiceInterface);
		Checker.notNull("parameter:remoteJsonClient", client);

		final GeneratorContext context = this.getGeneratorContext();

		final String methodName = method.getName();

		// build up a list of the parameter types for the async method...
		final List<Type> asyncMethodParameters = new ArrayList<Type>();
		final Iterator<MethodParameter> serviceMethodParameters = method.getParameters().iterator();
		while (serviceMethodParameters.hasNext()) {
			final MethodParameter methodParameter = serviceMethodParameters.next();
			asyncMethodParameters.add(methodParameter.getType());
		}
		asyncMethodParameters.add(this.getAsyncCallback());

		// make sure that a method with the same signature actually exists on
		// the async interface...
		Method asyncMethod = asyncServiceInterface.findMostDerivedMethod(methodName, asyncMethodParameters);
		if (null == asyncMethod) {
			// ...or exists in one of the extended interfaces
			for (Type iface:asyncServiceInterface.getInterfaces()) {
				asyncMethod = iface.findMostDerivedMethod(methodName, asyncMethodParameters);
				if (null != asyncMethod) {
					break;
				}
			}
		}
		if (null == asyncMethod) {
			this.throwMatchingAsyncInterfaceMethodNotFoundException(method);
		}
		if (false == asyncMethod.returnsVoid()) {
			this.throwIncompatibleMethodFound(asyncMethod);
		}
		context.debug("Found matching async interface method: " + asyncMethod);

		// create the method on client...
		final NewMethod newMethod = asyncMethod.copy(client);
		newMethod.setAbstract(false);
		newMethod.setFinal(true);

		// rename all parameters to parameterN
		GeneratorHelper.renameParametersToParameterN(newMethod);

		// the last parameter must be called "callback"
		final List parameters = newMethod.getParameters();
		final NewMethodParameter callback = (NewMethodParameter) parameters.get(parameters.size() - 1);
		callback.setName(Constants.CALLBACK_PARAMETER);

		context.debug("Finishing renaming parameters, parameters: " + parameters);

		return newMethod;
	}

	/**
	 * Visits all the public methods on the service interface implementing a
	 * proxy for each service method.
	 */
	protected void implementPublicMethods(final Type serviceInterface, final NewConcreteType client) {
		Checker.notNull("parameter:serviceInterface", serviceInterface);
		Checker.notNull("parameter:client", client);

		this.getGeneratorContext().info("Implementing async service interface methods");

		final Type asyncServiceInterface = this.getAsyncServiceInterface(serviceInterface);

		final AllMethodsVisitor publicMethodFinder = new AllMethodsVisitor() {
			protected boolean visit(final Method method) {
				JsonRpc11ClientGenerator.this.implementPublicMethod(method, serviceInterface, asyncServiceInterface, client);
				return false;
			}

			protected boolean skipJavaLangObjectMethods() {
				return true;
			}
		};
		// visit methods on the service interface
		publicMethodFinder.start(serviceInterface);
		// visit methods on the extended interfaces of the service interface
		// TODO handle methods with same signature
		for (Type iface:serviceInterface.getInterfaces()) {
			publicMethodFinder.start(iface);
		}
	}

}
